/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts;

import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.experimental.categories.Category;
import org.springframework.extensions.webscripts.DeclarativeRegistry;
import org.springframework.extensions.webscripts.Description;
import org.springframework.extensions.webscripts.Status;
import org.springframework.extensions.webscripts.TestWebScriptServer.DeleteRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.GetRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.PostRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.PutRequest;
import org.springframework.extensions.webscripts.TestWebScriptServer.Request;
import org.springframework.extensions.webscripts.TestWebScriptServer.Response;
import org.springframework.extensions.webscripts.WebScript;
import org.springframework.extensions.webscripts.WebScriptException;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
import org.alfresco.util.testing.category.LuceneTests;

/**
 * https://issues.alfresco.com/jira/browse/MNT-13180
 * 
 * Customer would like to be sure that we protect ourselves against unsanitized user inputs that can lead to XSS vulnerabilities. This probably means (at least) implementing a Unit Test framework that injects into each webscript listed at: http://localhost:8080/alfresco/wcservice/index/uri/ and for each documented parameter malicious input such as those used by our customer to detect the ones he found
 * 
 * @author Viachaslau Tsikhanovich
 */
@Category(LuceneTests.class)
public class XssVulnerabilityTest extends BaseWebScriptTest
{
    private Log logger = LogFactory.getLog(XssVulnerabilityTest.class);

    private DeclarativeRegistry webscriptsRegistry;

    static final String START_ARG = "{";
    static final String END_ARG = "}";

    static final String[] METHODS_TO_CHECK_ARRAY = {"GET", "DELETE", "POST", "PUT"};
    static final Set<String> METHODS_TO_CHECK_SET = new HashSet<String>(Arrays.asList(METHODS_TO_CHECK_ARRAY));
    static final String[] FORMATS_TO_CHECK_ARRAY = {"html"};
    static final Set<String> FORMATS_TO_CHECK_SET = new HashSet<String>(Arrays.asList(FORMATS_TO_CHECK_ARRAY));
    static final String[] URI_TO_SKIP_ARRAY = {".rss", ".atom"}; // javascript is not executed for feeds

    static final String MALARG1 = "<script>alert('XSS')</script>";
    static final String MALARG2 = "</script><script>alert('XSS')</script>";
    static final String MALARG3 = "\"</script><script>alert('XSS')</script>";
    static final String MALARG4 = "'\"</style></script><script>alert('XSS')</script>";
    static final String[] MALICIOUS_ARGS = {MALARG1, MALARG2, MALARG3, MALARG4};
    static final String[] SKIP_WEBSCRIPT_CHECK_ARRAY = {
            "org/alfresco/cmis/client/cmisbrowser/federatedquery.get", /** argument is put into form's textarea but javascript is not executed **/
            "org/alfresco/cmis/test.post.desc.xml"
    };
    static final Set<String> SKIP_WEBSCRIPT_CHECK_ID_SET = new HashSet<String>(Arrays.asList(SKIP_WEBSCRIPT_CHECK_ARRAY));

    protected void setUp() throws Exception
    {
        super.setUp();
        this.webscriptsRegistry = (DeclarativeRegistry) getServer().getApplicationContext().getBean("webscripts.registry.prototype");

        setDefaultRunAs(AuthenticationUtil.getAdminUserName());
    }

    protected void tearDown() throws Exception
    {
        super.tearDown();
    }

    protected Log getLogger()
    {
        return logger;
    }

    public void testXssVulnerability() throws Throwable
    {
        webscriptsRegistry.reset();
        final int scriptsSize = webscriptsRegistry.getWebScripts().size();
        int i = 0, successCount = 0, wserrcount = 0, vulnCount = 0;
        LinkedList<String> vulnerabileURLS = new LinkedList<String>();
        for (WebScript ws : webscriptsRegistry.getWebScripts())
        {
            if (getLogger().isDebugEnabled())
            {
                getLogger().debug("progress: " + ++i + "/" + scriptsSize);
            }

            Description wsDesc = ws.getDescription();
            if (SKIP_WEBSCRIPT_CHECK_ID_SET.contains(wsDesc.getId()))
            {
                // skip
                continue;
            }

            boolean isMethodCheck = METHODS_TO_CHECK_SET.contains(wsDesc.getMethod());
            boolean isFormatCheck = FORMATS_TO_CHECK_SET.contains(wsDesc.getDefaultFormat());
            if (isMethodCheck && isFormatCheck)
            {
                for (String malArg : MALICIOUS_ARGS)
                {
                    String[] uris = wsDesc.getURIs();
                    for (String uri : uris)
                    {
                        if (isUriSkip(uri))
                        {
                            continue;
                        }

                        // always parse url because we cannot rely on getArguments():
                        // - sometimes getArguments() returns null although URI has arguments
                        // - sometimes getArguments() returns set of args that does not contain args from url
                        List<String> parsedArgs = parseArgsFromURI(uri);
                        if (0 == parsedArgs.size())
                        {
                            // no arguments in uri, skip
                            continue;
                        }

                        String url = substituteMaliciousArgInURI(uri, parsedArgs, malArg);
                        Response resp;
                        try
                        {
                            resp = sendRequest(createRequest(wsDesc.getMethod(), url), -1);
                        }
                        catch (WebScriptException e)
                        {
                            // skip webscript errors
                            ++wserrcount;
                            continue;
                        }

                        String respString = resp.getContentAsString();
                        if (resp.getStatus() == Status.STATUS_OK)
                        {
                            ++successCount;
                        }

                        // do case insensitive check because argument can be converted to lowercase on page
                        if (respString.toLowerCase().contains(malArg.toLowerCase()))
                        {
                            vulnerabileURLS.add(wsDesc.getMethod() + " " + url);
                            vulnCount++;
                        }
                    }
                }
            }
        }

        if (getLogger().isDebugEnabled())
        {
            getLogger().debug("OK html responses count: " + successCount);
            getLogger().debug("Webscript errors count: " + wserrcount);
            getLogger().debug("Vulnerabile URLs count: " + vulnCount);
        }

        for (String url : vulnerabileURLS)
        {
            getLogger().warn("Vulnerabile URL: " + url);
        }
        assertTrue("Vulnerabile URLs found: " + vulnerabileURLS, vulnerabileURLS.size() == 0);
    }

    private boolean isUriSkip(String uri)
    {
        for (String uriPart : URI_TO_SKIP_ARRAY)
        {
            if (uri.contains(uriPart))
            {
                return true;
            }
        }
        return false;
    }

    private List<String> parseArgsFromURI(String uri)
    {
        List<String> args = new LinkedList<String>();
        int startBracketInd = uri.indexOf(START_ARG, 0);
        while (startBracketInd != -1)
        {
            int endBracketInd = uri.indexOf(END_ARG, startBracketInd);
            if (endBracketInd != -1)
            {
                String arg = uri.substring(startBracketInd + 1, endBracketInd);
                if (arg.endsWith("?"))
                {
                    // optional argument
                    arg = arg.substring(0, arg.length() - 1);
                }
                args.add(arg);
                // search next argument
                startBracketInd = uri.indexOf(START_ARG, endBracketInd);
            }
            else
            {
                // no ending bracket
                throw new AlfrescoRuntimeException("Invalid webscript URI : " + uri);
            }
        }
        return args;
    }

    private Request createRequest(String method, String url) throws Exception
    {
        switch (method)
        {
        case "DELETE":
            return new DeleteRequest(url);
        case "GET":
            return new GetRequest(url);
        case "PUT":
            return new PutRequest(url, "{}", "application/json");
        case "POST":
            return new PostRequest(url, "{}", "application/json");
        default:
            throw new InvalidArgumentException("HTTP method not supported");
        }
    }

    private String substituteMaliciousArgInURI(String uri, List<String> urlArgs, String malArg)
    {
        String url = uri;

        // substitute malicious arguments
        for (String arg : urlArgs)
        {
            url = url.replace(START_ARG + arg + END_ARG, "a" + malArg);
            url = url.replace(START_ARG + arg + "?" + END_ARG, "a" + malArg);
        }

        if (url.contains(START_ARG) || url.contains(END_ARG))
        {
            throw new AlfrescoRuntimeException("Arguments were not properly substituted: " + url);
        }

        return url;
    }

}
